Skip to content

在 WPF MVVM Application 中使用 Dependency Injection

TLDR

  • 在 WPF 中引入 Microsoft.Extensions.DependencyInjection 可實現現代化的依賴注入,取代傳統的 Singleton 或手動管理。
  • 使用 CommunityToolkit.Mvvm 可簡化 MVVM 架構開發,透過 Source Generators 自動產生屬性與指令。
  • 必須移除 App.xaml 中的 StartupUri 屬性,改由 OnStartup 手動建立 MainWindow,否則會因建構函式參數注入失敗而報錯。
  • ViewModel 需繼承 ObservableObject 並宣告為 partial class,以便使用 [ObservableProperty][RelayCommand] 屬性。
  • 綁定時需注意命名慣例:[ObservableProperty] 產生的屬性為 Pascal Case,[RelayCommand] 產生的指令需加上 Command 後綴。

在 WPF 使用 Dependency Injection

在 WPF 專案中引入 DI 容器(如 Microsoft.Extensions.DependencyInjection)時,必須調整應用程式的啟動流程。

App.xaml 設定與啟動邏輯

什麼情況下會遇到這個問題:當你將 MainWindow 改為透過 DI 容器注入依賴(例如在建構函式中注入 ViewModel)時。

由於 WPF 預設的 StartupUri 機制會嘗試呼叫無參數的建構函式,若改用 DI 建立 MainWindow,必須移除 App.xaml 中的 StartupUri 屬性,並在 App.xaml.csOnStartup 方法中手動初始化:

csharp
public partial class App : Application {
    protected override void OnStartup(StartupEventArgs e) {
        IConfigurationBuilder builder = new ConfigurationBuilder()
            .SetBasePath(Directory.GetCurrentDirectory())
            .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);

        IConfiguration configuration = builder.Build();

        ServiceCollection serviceCollection = new ServiceCollection();
        ConfigureServices(serviceCollection, configuration);

        ServiceProvider serviceProvider = serviceCollection.BuildServiceProvider();

        MainWindow mainWindow = serviceProvider.GetRequiredService<MainWindow>()!;
        mainWindow.Show();
    }

    private static void ConfigureServices(IServiceCollection services, IConfiguration configuration) {
        services.Configure<AppOptions>(configuration!.GetSection("App"));
        services.AddTransient<MainWindow>();
        services.AddTransient<ViewModel>();
    }
}

建立 WPF MVVM Application

使用 CommunityToolkit.Mvvm 可以大幅減少樣板程式碼。

ViewModel 實作規範

什麼情況下會遇到這個問題:當你需要實作資料綁定與指令,但不想手寫繁瑣的 INotifyPropertyChanged 實作時。

ViewModel 必須符合以下規範:

  • 繼承 ObservableObject
  • 宣告為 partial class。
  • 使用 [ObservableProperty] 標記欄位(Source Generator 會自動產生對應的 Pascal Case 屬性)。
  • 使用 [RelayCommand] 標記方法(Source Generator 會自動產生對應的 Command 屬性)。
csharp
public partial class ViewModel : ObservableObject {
    [ObservableProperty]
    private string? input;

    [RelayCommand]
    private void Submit() {
        MessageBox.Show("輸入值:" + Input);
        Input += "_修改";
        MessageBox.Show("變更值:" + Input);
    }
}

observable property attribute

relay command attribute

MainWindow 綁定設定

什麼情況下會遇到這個問題:當你在 XAML 中進行資料綁定時,若未遵循自動產生的命名規則,會導致綁定失敗。

MainWindow 的建構函式中注入 ViewModel 並設定 DataContext

csharp
public partial class MainWindow : Window {
    public MainWindow(ViewModel viewModel) {
        InitializeComponent();
        DataContext = viewModel;
    }
}

在 XAML 中,綁定名稱需對應 Source Generator 產生的結果:

  • TextBox 綁定 Input(對應 input 欄位)。
  • Button 綁定 SubmitCommand(對應 Submit 方法)。
xml
<Window x:Class="WpfApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <TextBox Text="{Binding Input}"/>
        <Button Content="送出" Command="{Binding SubmitCommand}"/>
    </Grid>
</Window>

執行結果

  • 點擊「送出」後,Submit() 方法成功執行並取得 Input 值。
  • Submit() 修改 Input 屬性時,畫面的 TextBox 會自動同步更新。

wpf di demo input

wpf di demo success

wpf di demo update

異動歷程

    • 初版文件建立。